24  药品销售数据分析

24.1 引言药品销售数据分析的价值

医院药品销售分析:

  • 库存管理: 优化药品采购
  • 处方分析: 医生开药习惯
  • 医保分析: 社保使用情况
  • 销售预测: 季节性需求

24.2 本章学习目标

本章以医院药品销售流水为对象,练习围绕业务提问组织分析。通过本章学习,你将掌握:

  1. Excel销售数据的读取与体检:shapeheadinfo、缺失值与重复值检查
  2. 从”购药时间”字符串切分日期与星期、再从日期提取月份的特征构造方法
  3. groupby + sum + nlargest/nsmallest 的排名式聚合
  4. sns.lineplot(estimator='sum') 与柱状图呈现月度、星期销售结构与TOP榜
  5. 用”应收金额-实收金额”的差额构造社保减免标志并统计占比

先修内容:第 章节 14 章(Excel文件读取)与第 章节 13 章(分组聚合);图形绘制回看第 章节 20 章。

24.3 数据预处理

任务要求:读取平台内置的”朝阳医院2018年销售数据.xlsx”,完成类型转换与缺失值、重复值检查后,依次回答四个业务问题——(1)按实收金额总和找出销售最好与最差的药品,并绘制销售金额TOP10柱状图;(2)从购药时间提取月份与星期,分别绘制月度、星期销售额折线图;(3)逐月找出当月实收金额最高的药品;(4)用应收金额与实收金额的差额构造”是否社保减免药品”标志,并统计其占比。

以下平台原始代码在任务3的循环体内存在缩进错误,属平台题面原样,请按平台原始题面原样输入,以平台判定为准。

列表 24.1: 平台原始代码
# 注:朝阳医院2018年销售数据.xlsx数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:该代码块存在缩进错误,请按平台原始题面原样输入,以平台判定为准
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import seaborn as sns  # 导入Seaborn可视化库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

#数据读取
df = pd.read_excel("朝阳医院2018年销售数据.xlsx")

#数据预览与预处理
## 查看数据维度
print(df.shape)
print(df.head())  # 输出前几行数据

## 查看数据信息  
print(df.info())

df['社保卡号']=df['社保卡号'].astype(str)  # 转换数据类型
df['商品编码']=df['商品编码'].astype(str)  # 转换数据类型
df['购药日期']=df['购药时间'].str.split(pat=' ',expand=True)[0]  # 对字符串列进行处理
df['购药星期']=df['购药时间'].str.split(pat=' ',expand=True)[1]  # 对字符串列进行处理

## 查看各列的缺失值
df.isnull().sum()

## 缺失值处理
## 直接删除
df.dropna(axis=0,inplace=True)

## 查看重复值数量
df.duplicated().sum()

#探索性数据分析
##任务1:销售情况最好的药品是?最差的药品是?
##提示:通过对药品进行分组聚合统计,在此处我们评定销售情况好的标准是实收金额总和。之后取出前n个药品进行可视化展示

df_sum=df.groupby('商品名称')['实收金额'].sum().reset_index()  # 按指定列分组聚合
print(df_sum.nlargest(2,'实收金额')) #用于获取实收金额列中最大的两个值所对应的行。
print(df_sum.nsmallest(2,'实收金额')) #用于获取实收金额列中最小的两个值所对应的行。

df_sum_n10=df_sum.nlargest(10,'实收金额')  # 筛选出排名前10的记录
plt.figure(figsize=(15,10))  # 创建图形画布
plt.grid()  # 显示网格线
plt.bar(df_sum_n10['商品名称'],df_sum_n10['实收金额'])  # 绘制柱状图
plt.savefig("1.png")  # 保存图形至文件
plt.close()  # 关闭文件或连接

#任务2:整体销售额的时间变化趋势
##数据集中存在时间数据,但是不够简洁,所以需要先对时间数据进行处理:

df["购药时间_月"] = df["购药时间"].map(lambda x:x.split(" ")[0].split("-")[1]) ##购药日期所属月份   
df["购药时间_星期"] = df["购药时间"].map(lambda x:x.split(" ")[1]) ##购药当天是星期几? 

#对时间数据处理完成之后,就可以进行时间维度上的销售情况分析,与任务1一致,评定指标也选择为实收金额之和。

# 在下方开始你的分析
df['购药日期_月']=df['购药日期'].str.split(pat='-',expand=True)[1]
plt.figure(figsize=(15,10))  # 创建图形画布
plt.grid()  # 显示网格线
sns.lineplot(x='购药日期_月',y='实收金额',data=df,estimator='sum')  # 绘制折线图
plt.savefig("2.png")  # 保存图形至文件
plt.close()  # 关闭文件或连接

plt.figure(figsize=(15,10))  # 创建图形画布
plt.grid()  # 显示网格线
sns.lineplot(x='购药星期',y='实收金额',data=df,estimator='sum')  # 绘制折线图
plt.savefig("3.png")  # 保存图形至文件
plt.close()  # 关闭文件或连接

#任务3:各月份卖的最好的药品是那些?
values1=df['购药日期_月'].unique()
values1=pd.Series(values1)  # 创建Series序列values1
for value in values1:  # 遍历values1中的每个value
    df_value=df[df['购药日期_月']==value]  # 提取购药日期_月列作为df_value变量
  df_print=df_value.groupby(['购药日期_月','商品名称'])['实收金额'].sum().nlargest(1).reset_index()  # 按指定列分组聚合
    print(df_print)  # 输出数据框数据

#任务4:人们是否会更愿意购买可使用社保减免的药品?
df['差额']=df['应收金额']-df['实收金额']
df['是否社保减免药品']=df['差额'].apply(lambda x : '是' if x!=0 else '否')  # 定义匿名函数df['是否社保减免药品']
df['是否社保减免药品'].value_counts(normalize=True)  # 统计是否社保减免药品列各取值的频率分布

#####总结
# 卖的最好的是 开博通
# 卖的最差的是 TG厄贝沙坦片
# 药品的销售数量与时间有关
# 药品的销售数量与是否使用社保有关

预期输出:数据文件平台内置,本地无数据,具体数值以平台运行结果为准。判读要点:

  1. 文本输出:数据形状、info 摘要、各列缺失值计数;销售最好与最差的各2种药品;逐月的当月最佳药品清单;“是否社保减免药品”的占比频率表。
  2. 图形输出(1.png):销售金额TOP10药品柱状图——看头部药品之间的量级差距与断层。图形输出(2.png、3.png):月度、星期销售额折线图——看峰值月份、低谷月份,以及工作日与周末的差异。
  3. 逐月最佳药品清单应与月度折线图互证:清单里的药品是否正是把对应月份销售额抬高的主力。
  4. 平台题面在任务3的循环体内存在缩进错误,按平台原始题面原样输入,以平台判定为准。

下方提供一个本地演练版:由于”朝阳医院2018年销售数据.xlsx”仅平台内置,本地以一份 12 行的同结构内联合成迷你表代替(列名与平台数据一致,购药时间同样为”日期 空格 星期”格式,部分行应收金额大于实收金额以保留社保减免的差额形态),并在其上复现平台任务1”按实收金额对药品排名”的同型输出:

列表 24.2: 本地演练版(合成数据)
# 本地演练说明:平台内置的朝阳医院2018年销售数据.xlsx本地没有,以下用同结构的内联合成迷你数据代替
import pandas as pd  # 导入Pandas数据分析库
df_mini = pd.DataFrame({  # 构造12行迷你表,列结构同平台数据
    '社保卡号': ['1001', '1002', '1003', '1004', '1005', '1006', '1007', '1008', '1009', '1010', '1011', '1012'],  # 社保卡号(文本型)
    '商品编码': ['P001', 'P002', 'P003', 'P001', 'P002', 'P004', 'P001', 'P003', 'P002', 'P004', 'P001', 'P003'],  # 商品编码(文本型)
    '商品名称': ['开博通', '氨苄西林胶囊', 'TG厄贝沙坦片', '开博通', '氨苄西林胶囊', 'VC银翘片',
                '开博通', 'TG厄贝沙坦片', '氨苄西林胶囊', 'VC银翘片', '开博通', 'TG厄贝沙坦片'],  # 商品名称
    '销售数量': [5, 2, 1, 3, 4, 2, 6, 1, 3, 2, 4, 1],  # 销售数量
    '应收金额': [316.0, 74.4, 19.8, 189.6, 148.8, 45.6, 379.2, 19.8, 111.6, 45.6, 252.8, 19.8],  # 应收金额(原价)
    '实收金额': [281.0, 66.96, 19.8, 189.6, 133.92, 45.6, 379.2, 19.8, 100.44, 45.6, 252.8, 19.8],  # 实收金额(部分行小于应收,即社保减免)
    '购药时间': ['2018-01-05 星期五', '2018-01-08 星期一', '2018-01-12 星期五', '2018-02-02 星期五', '2018-02-05 星期一', '2018-02-09 星期五',
                '2018-03-02 星期五', '2018-03-05 星期一', '2018-03-09 星期五', '2018-03-12 星期一', '2018-03-16 星期五', '2018-03-19 星期一']  # 日期+星期,同平台格式
})
df_sum = df_mini.groupby('商品名称')['实收金额'].sum().reset_index()  # 与平台任务同口径:按商品名称聚合实收金额总和
print('按实收金额的药品排名(降序):')
print(df_sum.sort_values('实收金额', ascending=False).reset_index(drop=True))  # 全量排名表
print('销售最好的2种药品:')
print(df_sum.nlargest(2, '实收金额').reset_index(drop=True))  # 前两名,同平台任务1
print('销售最差的2种药品:')
print(df_sum.nsmallest(2, '实收金额').reset_index(drop=True))  # 后两名,同平台任务1

预期输出(本机实际运行结果):

按实收金额的药品排名(降序):
      商品名称     实收金额
0      开博通  1102.60
1   氨苄西林胶囊   301.32
2    VC银翘片    91.20
3  TG厄贝沙坦片    59.40
销售最好的2种药品:
     商品名称     实收金额
0     开博通  1102.60
1  氨苄西林胶囊   301.32
销售最差的2种药品:
      商品名称  实收金额
0  TG厄贝沙坦片  59.4
1    VC银翘片  91.2

判读要点与平台任务1同型:开博通按实收金额总和居首、TG厄贝沙坦片垫底;nlargest(2)/nsmallest(2) 的结果与降序排名表的头尾两行互相印证。

24.4 任务1销售情况最好的药品

平台判定代码按各药品的实收金额总和聚合排定销售名次(题面提示原文为“销售数量总和”,与判定代码口径不一致时以平台判定为准)。口径既然如此关键,本节不再逐行重述任务1的聚合与绘图代码,改做一个变式实验:把聚合列这一个参数由 实收金额 换成 销售数量,其余代码一行不动,再看”销售最好的药品”还是不是同一个答案。实验在本地合成数据上运行(15行,列结构同平台数据;其中VC银翘片被设定为单价约10元的低价常用药、开博通为单价约60元的高价药,以放大两种口径的差异),请重点观察对比表中”金额口径名次”与”数量口径名次”两列的错位。

列表 24.3: 变式实验:改用销售数量口径排名
# 本地演练说明:朝阳医院2018年销售数据.xlsx仅平台内置,以下15行合成数据与平台数据同结构
import pandas as pd  # 导入Pandas数据分析库
df_rank = pd.DataFrame({  # 15行合成数据,列结构同平台数据;VC银翘片为低价常用药、开博通为高价药,以放大两种口径的差异
    '商品名称': ['开博通', '开博通', '开博通', '开博通', '氨苄西林胶囊', '氨苄西林胶囊', '氨苄西林胶囊', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片'],  # 商品名称
    '销售数量': [5, 3, 6, 4, 2, 4, 3, 1, 1, 1, 2, 2, 5, 5, 5],  # 每笔销售的盒数
    '应收金额': [316.0, 189.6, 379.2, 252.8, 74.4, 148.8, 111.6, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3],  # 应收金额(原价)
    '实收金额': [281.0, 189.6, 379.2, 252.8, 66.96, 133.92, 100.44, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3],  # 实收金额(部分行小于应收,即社保减免)
    '购药时间': ['2018-01-05 星期五', '2018-02-02 星期五', '2018-03-02 星期五', '2018-03-16 星期五', '2018-01-08 星期一', '2018-02-05 星期一', '2018-03-09 星期五', '2018-01-12 星期五', '2018-03-05 星期一', '2018-03-19 星期一', '2018-01-15 星期一', '2018-02-09 星期五', '2018-01-19 星期五', '2018-02-12 星期一', '2018-03-23 星期五']  # 日期+星期,同平台格式
})
amt = df_rank.groupby('商品名称')['实收金额'].sum().rename('实收金额总和')  # 金额口径:与平台任务1判定代码完全一致
qty = df_rank.groupby('商品名称')['销售数量'].sum().rename('销售数量总和')  # 数量口径:仅把聚合列换成销售数量,其余代码不动
compare = pd.concat([qty, amt], axis=1)  # 两种口径并排放进同一张表
compare['金额口径名次'] = compare['实收金额总和'].rank(ascending=False, method='min').astype(int)  # 按金额总和排名
compare['数量口径名次'] = compare['销售数量总和'].rank(ascending=False, method='min').astype(int)  # 按销售数量排名
print(compare.sort_values('金额口径名次'))  # 输出对比表,观察两个名次列的错位
print('\n金额口径的销售冠军:', compare['实收金额总和'].idxmax(), '| 数量口径的销售冠军:', compare['销售数量总和'].idxmax())  # 两种口径各自的冠军

预期输出(本机实际运行结果):

         销售数量总和   实收金额总和  金额口径名次  数量口径名次
商品名称                                    
开博通          18  1102.60       1       2
氨苄西林胶囊        9   301.32       2       3
VC银翘片        19   194.94       3       1
TG厄贝沙坦片       3    59.40       4       4

金额口径的销售冠军: 开博通 | 数量口径的销售冠军: VC银翘片

两种口径给出了两个不同的”销售冠军”:按实收金额是开博通(1102.60元),按销售数量是VC银翘片(19盒),四种药品中有三种的名次发生了移动。名次错位的根源是单价:高价药以金额取胜,低价常用药以盒数取胜。这正是”口径先于代码”的具体含义——回答”哪种药占用患者最多药费、贡献最多收入”用金额口径;回答”哪种药使用频次最高、需要按盒备货”用数量口径。到平台上做题时,题面提示与判定代码口径不一致,以平台判定为准;但在自己的分析报告里,应当先声明口径再报排名。

24.5 任务2时间变化趋势

列表 24.4: 任务2:整体销售额的时间变化趋势
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目:销售额的时间序列趋势分析
# =============================================================================
# 本代码分析整体销售额随时间的变化趋势,包括月度趋势和星期趋势。
# 时间序列分析可以识别季节性规律、周期性波动和异常点,为销售预测
# 和资源规划提供依据。

# ==================== 提取月份特征 ====================
# 从购药日期列中提取月份
df['购药日期_月'] = df['购药日期'].str.split(pat='-', expand=True)[1]
# str.split('-')按短横线分割日期字符串
# expand=True将分割结果展开为DataFrame
# [1]取第二列(月份部分),例如:"2018-01-01" → "01"

# ==================== 绘制月度销售趋势 ====================
# 创建画布,尺寸为15x10英寸
plt.figure(figsize=(15, 10))

# 使用Seaborn绘制月度销售趋势线图
sns.lineplot(x='购药日期_月', y='实收金额', data=df, estimator='sum', errorbar=None)
# x='购药日期_月'指定x轴数据(月份)
# y='实收金额'指定y轴数据(销售金额)
# data=df指定数据源
# estimator='sum'表示对每个月的销售额求和
# errorbar=None表示不显示误差条(置信区间)

# 设置图表标题
plt.title('各月销售额变化趋势', fontsize=16)  # 标题说明分析内容

# 设置x轴标签
plt.xlabel('月份', fontsize=12)  # x轴表示月份(1-12)

# 设置y轴标签
plt.ylabel('销售金额(元)', fontsize=12)  # y轴表示销售金额,注明单位

# 添加网格线,便于读取数值
plt.grid(True, alpha=0.3)  # alpha=0.3设置网格线透明度

# 自动调整布局
plt.tight_layout()  # 防止标签被截断

# 显示图表
plt.show()  # 展示完整的月度趋势图

# ==================== 绘制星期销售趋势 ====================
# 创建新的画布
plt.figure(figsize=(15, 10))

# 使用Seaborn绘制星期销售趋势线图
sns.lineplot(x='购药星期', y='实收金额', data=df, estimator='sum', errorbar=None)
# x='购药星期'指定x轴数据(星期几)
# y='实收金额'指定y轴数据(销售金额)
# estimator='sum'表示对每个星期的销售额求和
# errorbar=None不显示误差条

# 设置图表标题
plt.title('各星期销售额变化趋势', fontsize=16)

# 设置x轴标签
plt.xlabel('星期', fontsize=12)  # x轴表示星期(星期一至星期日)

# 设置y轴标签
plt.ylabel('销售金额(元)', fontsize=12)

# 添加网格线
plt.grid(True, alpha=0.3)

# 自动调整布局
plt.tight_layout()

# 显示图表
plt.show()  # 展示完整的星期趋势图

# ==================== 输出解读 ====================
# 时间趋势分析揭示销售的时间规律:
#
# 1. 月度趋势:
#    - 识别季节性规律:某些月份销售显著高于其他月份
#    - 可能原因:流感季(冬春)、过敏季(春季)、寒暑假(学生购药减少)
#    - 用途:根据预测调整库存,避免缺货或积压
#
# 2. 星期趋势:
#    - 工作日vs周末:工作日销售可能更高(上班族就诊)
#    - 周初vs周末:周一可能最高(周末积累的病人)
#    - 用途:合理安排医生排班和药房人手
#
# 3. 异常点:
#    - 突然的峰值:可能因疫情爆发、流感流行
#    - 突然的低谷:可能因节假日、医院停诊
#    - 需要结合外部事件分析
#
# 4. 业务决策:
#    - 库存管理:高峰月提前备货
#    - 人员安排:高峰日增加人手
#    - 营销策略:淡季推出促销活动
#    - 预测模型:基于历史数据预测未来销售

24.6 任务3各月份最佳药品

列表 24.5: 任务3:各月份卖的最好的药品是那些?
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目:识别每个月的销售冠军药品
# =============================================================================
# 本代码分析每个月销售最好的药品,识别季节性畅销品。不同季节可能
# 伴随不同的疾病,导致相关药品销售上升。这种分析可以指导季节性
# 备货和营销策略。

# ==================== 获取所有月份 ====================
# 提取购药日期_月列的所有唯一值(去重后的月份列表)
months = df['购药日期_月'].unique()  # unique()返回唯一值数组
# 例如:['01', '02', '03', ..., '12']

# ==================== 遍历每个月,找出最佳药品 ====================
for month in sorted(months):  # sorted()对月份排序,确保按1-12的顺序输出
    # 筛选该月份的所有销售记录
    df_month = df[df['购药日期_月'] == month]  # 布尔索引,筛选特定月份的行

    # 按月份和商品名称分组,计算销售金额,找出第一名
    df_best = df_month.groupby(['购药日期_月', '商品名称'])['实收金额'].sum().nlargest(1).reset_index()
    # groupby(['购药日期_月', '商品名称'])按月份和药品分组
    # ['实收金额']选择要聚合的列
    # .sum()计算每组的销售总额
    # .nlargest(1)提取销售金额最大的1行(即该月的销售冠军)
    # .reset_index()重置索引

    # 打印该月的最佳药品
    print(f'{month}月最佳药品:')
    print(df_best)  # 显示月份、药品名称、销售金额
    print()  # 打印空行,分隔不同月份的输出

# ==================== 输出解读 ====================
# 各月最佳药品分析揭示季节性用药规律:
#
# 1. 季节性疾病:
#    - 冬季(12-2月):感冒药、退烧药销售冠军
#    - 春季(3-5月):抗过敏药销售冠军
#    - 夏季(6-8月):消化药、防暑药销售冠军
#    - 秋季(9-11月):止咳药、抗生素销售冠军
#
# 2. 备货策略:
#    - 提前1个月备货季节性药品
#    - 避免高峰期缺货影响患者
#    - 减少非季节性药品的库存积压
#
# 3. 营销策略:
#    - 在季节到来前推出相关促销
#    - 制作季节性用药指南,指导患者
#    - 与医生沟通,提前处方调整
#
# 4. 采购决策:
#    - 与供应商签订季节性供货协议
#    - 确保高峰期的药品供应
#    - 争取批量采购的折扣

24.7 任务4社保减免分析

列表 24.6: 任务4:人们是否会更愿意购买可使用社保减免的药品?
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目:分析社保减免对药品购买意愿的影响
# =============================================================================
# 本代码通过对比应收金额和实收金额,判断药品是否使用了社保减免,
# 并统计使用社保减免的药品占比。这有助于了解患者对医保政策的响应,
# 以及医保政策对药品销售的影响。

# ==================== 计算差额 ====================
# 计算应收金额和实收金额的差额
df['差额'] = df['应收金额'] - df['实收金额']
# 应收金额:药品原价
# 实收金额:患者实际支付的金额
# 差额:社保报销的金额
# 差额>0表示使用了社保减免,差额=0表示未使用

# ==================== 判断是否使用社保 ====================
# 根据差额判断是否使用社保减免
df['是否社保减免药品'] = df['差额'].apply(lambda x: '是' if x != 0 else '否')
# apply()函数对Series的每个元素应用指定函数
# lambda x: '是' if x != 0 else '否'是一个匿名函数
# 如果差额不为0,返回'是';否则返回'否'
# 结果是一个新的分类变量

# ==================== 统计社保减免比例 ====================
# 计算'是'和'否'的数量和占比
insurance_ratio = df['是否社保减免药品'].value_counts(normalize=True)
# value_counts()计算每个类别的数量
# normalize=True返回占比而非绝对数量
# 结果格式: {'是': 0.75, '否': 0.25}

# 打印社保减免药品的占比
print('社保减免药品占比:')
print(insurance_ratio)  # 显示'是'和'否'的占比

# 提取'是'的占比,并转换为百分比
print(f'\n使用社保减免的药品占比: {insurance_ratio.get("是", 0)*100:.1f}%')
# insurance_ratio.get("是", 0)获取'是'的占比,如果不存在则返回0
# * 100转换为百分比
# :.1f保留1位小数

# ==================== 输出解读 ====================
# 社保减免分析揭示医保政策的影响:
#
# 1. 占比解读:
#    - 如果社保减免占比很高(如75%以上),说明:
#      - 患者倾向于使用医保购买药品
#      - 医保政策覆盖面广,患者受益大
#      - 药品大部分在医保目录内
#
# 2. 患者行为:
#    - 价格敏感度:有医保减免时,患者更愿意购买
#    - 药品选择:优先选择医保目录内的药品
#    - 购买意愿:医保降低了经济负担,增加了用药需求
#
# 3. 医院策略:
#    - 药品采购:优先采购医保目录内的药品
#    - 处方管理:医生优先开医保药品,提高患者满意度
#    - 库存管理:医保药品库存要充足
#
# 4. 政策影响:
#    - 医保目录调整会影响药品销售
#    - 报销比例变化会影响患者选择
#    - 需要密切关注医保政策变化
#
# 5. 商业机会:
#    - 对于非医保药品,需要突出其独特价值
#    - 可以开发医保目录外的替代药品
#    - 提供自费患者的增值服务

24.8 分析结论

  1. 销售冠军: 开博通
  2. 销售垫底: TG厄贝沙坦片
  3. 季节性: 药品销售与时间相关
  4. 医保影响: 社保减免显著影响购买意愿

24.9 本章小结

本章新增的技能要点:

  1. 字符串时间列的两步拆解:先按空格切出”日期+星期”,再按”-“取出月份,得到可供分组的时间维度。
  2. 排名类问题优先用 nlargest/nsmallest:比”排序后取头”意图更直接,也避免改动原表。
  3. sns.lineplot(estimator='sum'):对分类x轴直接做分组求和绘图,省去先聚合再画的步骤。
  4. 口径先于代码:题面提示写”销售数量总和”、判定代码却按”实收金额总和”聚合,动手前必须对齐口径(以平台判定为准)。
  5. 差额构造标志列:“应收-实收”是否为0是本章判断社保减免的依据,这类”由两列推导第三列”的思路在业务数据中反复出现。

易错点:

  • 不核对口径就按题面提示的”销售数量”聚合:平台判定按”实收金额总和”,金额口径偏高价药、数量口径偏低价常用药,两个口径会给出不同的”销售冠军”
  • sns.lineplot 漏写 estimator='sum':默认统计量是均值,画出的折线是各月平均单笔金额而非月度总额
  • 提取月份时越级取错段:须先按空格切出日期、再按”-“取第2段;直接对”购药时间”整串取第3段会把”05 星期五”连日期带星期混进月份列
  • 任务3循环体内的缩进错误是平台题面原样:不要”顺手修正”后再提交,平台只认原始题面
  • 并列名次跨越截断点时,nlargest(2)sort_values().head(2) 都不扩展并列,后者默认非稳定排序、并列顺序无承诺;要”并列都取”须改用 rank(method='min')
  • 把”社保减免药品占比高”直接读成”人们更愿意购买可社保减免的药品”:占比只说明销售构成,推断购买意愿还须交代按订单数还是金额、完全自费还是部分自费的口径

24.10 动手与思考

以下练习每题附参考答案(默认折叠);概念题与变式题可对照自查,商业判断题不设唯一答案,判断依据与口径比结论更重要。

  1. 概念辨析:“销售情况最好”用实收金额总和与用销售数量总和衡量,结论可能有何不同?两种口径分别适合回答什么业务问题?

    参考答案(点开前请先独立完成)

    解题思路:两种口径可能给出不同的“销售冠军”。金额口径被高价药主导——单价高的药品即使销量不大,金额总和也容易居前;数量口径被低价常用药主导——单盒便宜的常用药开得最频繁。本章“任务1”一节的变式实验(列表 24.3)在合成数据上实测过这种错位:按实收金额总和,开博通第一;按销售数量总和,VC银翘片第一,四种药品里有三种名次发生移动。口径选择跟着业务问题走:要回答“哪种药占用患者最多药费、贡献最多收入”用金额口径;要回答“哪种药使用频次最高、需要按盒备货”用数量口径。题面提示与平台判定代码口径不一致时以平台判定为准,但在自己的分析报告里应当先声明口径再报排名。

    回扣本章:对应“本章新增的技能要点”第4条(口径先于代码)。

  2. 概念辨析:df_sum.nlargest(2, '实收金额')df_sum.sort_values('实收金额', ascending=False).head(2) 在什么情况下结果一致、什么情况下不一致(提示:并列名次)?

    参考答案(点开前请先独立完成)

    解题思路:当“实收金额”取值在截断点附近没有并列时(比如前几名金额互不相同),两种写法返回的行集合与顺序完全一致。当并列跨越截断点时,两者都只返回2行、不做并列扩展,差别在并列行的取舍规则:nlargest 有显式的 keep 参数(默认 keep='first',并列时保留先出现的行),而 sort_values 默认使用非稳定排序(kind 默认为 ‘quicksort’),对并列值的先后顺序不做承诺——同一份小数据上可能恰好与 nlargest 一致,换一份数据就可能取到另一条并列行或行序不同(实测给 sort_valueskind='stable' 之后,两者行序完全一致)。如果业务要求“并列的都取”,两种写法都不满足,应改用 rank(ascending=False, method='min') 先标记同名次、再按名次筛选。综合来看,nlargest 把并列取舍写成了明确规则、意图也更直接,这是本章把它列为排名首选写法的原因。

    回扣本章:对应“本章新增的技能要点”第2条(排名类问题优先用 nlargest/nsmallest,行为规则明确且不改动原表)。

  3. 变式任务:把任务1的评定指标换成销售数量总和,重做TOP10柱状图——需要修改哪些代码行?

    参考答案(点开前请先独立完成)

    改造思路:任务1的代码里所有出现“实收金额”的地方共三处需要同步替换——groupby 后的聚合列、nlargest/nsmallest 的排序依据列、plt.bar 的柱高数据列;变量名建议同步改为 df_qty,避免与原任务的 df_sum 混淆。数据读取、缺失值处理等前段一行不动。

    # 变式代码:任务1的评定指标由实收金额换成销售数量(仅列改动行,其余与平台任务一致)
    df_qty = df.groupby('商品名称')['销售数量'].sum().reset_index()  # ← 改动点1:聚合列由“实收金额”改为“销售数量”
    print(df_qty.nlargest(2, '销售数量'))  # ← 改动点2:销售最好两名的取数列同步替换
    print(df_qty.nsmallest(2, '销售数量'))  # ← 改动点2:销售最差两名同上
    df_qty_n10 = df_qty.nlargest(10, '销售数量')  # ← 改动点2:TOP10取数列同步替换
    plt.figure(figsize=(15, 10))  # 与平台任务一致
    plt.grid()  # 与平台任务一致
    plt.bar(df_qty_n10['商品名称'], df_qty_n10['销售数量'])  # ← 改动点3:柱高改为销售数量列
    plt.savefig('1_qty.png')  # 建议另存新文件名,避免覆盖平台任务的1.png
    plt.close()  # 与平台任务一致

    结构性判读:图形仍是一张TOP10柱状图,但柱高含义由“实收金额总和”变为“销售数量总和”,柱的排序与高低差随之改变。判读要点:与原任务的1.png对照——头部药品是否换了名、量级断层是否移动;若两种口径的冠军不同,说明存在低价高频药与高价低频药的分化,备货口径(按盒)与营收口径(按元)应分别参考各自的排名。

    本地演练版(模拟数据):在与正文变式实验同一份15行合成数据上实跑(列结构同平台数据):

    # 本地演练版:销售数量口径的药品排名(数据与正文@lst-ch24-ranking-metric-variant一致)
    import pandas as pd  # 导入Pandas数据分析库
    df_rank = pd.DataFrame({  # 15行合成数据:VC银翘片为低价常用药、开博通为高价药,以放大两种口径的差异
        '商品名称': ['开博通', '开博通', '开博通', '开博通', '氨苄西林胶囊', '氨苄西林胶囊', '氨苄西林胶囊', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'TG厄贝沙坦片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片', 'VC银翘片'],
        '销售数量': [5, 3, 6, 4, 2, 4, 3, 1, 1, 1, 2, 2, 5, 5, 5],  # 每笔销售的盒数
        '实收金额': [281.0, 189.6, 379.2, 252.8, 66.96, 133.92, 100.44, 19.8, 19.8, 19.8, 20.52, 20.52, 51.3, 51.3, 51.3]  # 实收金额
    })
    df_qty = df_rank.groupby('商品名称')['销售数量'].sum().reset_index()  # 变式改动点1:聚合列改为销售数量
    print('数量口径的药品排名(降序,柱状图将按此顺序绘柱):')
    print(df_qty.sort_values('销售数量', ascending=False).reset_index(drop=True))  # 变式改动点2:排序与柱高均改用数量列
    print('数量口径TOP2:', df_qty.nlargest(2, '销售数量')['商品名称'].tolist())  # 与平台任务1同型的TOP取数

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

    数量口径的药品排名(降序,柱状图将按此顺序绘柱):
          商品名称  销售数量
    0    VC银翘片    19
    1      开博通    18
    2   氨苄西林胶囊     9
    3  TG厄贝沙坦片     3
    数量口径TOP2: ['VC银翘片', '开博通']

    演练判读:数量口径的冠军换成VC银翘片(19盒),与正文中金额口径冠军开博通不同——同一份数据、两种口径、两个“销售冠军”,印证了第1题的结论。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 24.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第2条(nlargest 换一个排序依据列即可切换口径)与第4条(口径先于代码)。

  4. 变式任务:把任务2的星期维度换成”上旬/中旬/下旬”三段,特征构造与绘图的改造思路是什么?

    参考答案(点开前请先独立完成)

    改造思路:分两步。特征构造上,沿用任务2“字符串两步拆解”的思路再多走一步:先按空格切出日期段,再按“-”取出“日”并转成整数,然后用 pd.cut 把1—10日、11—20日、21—31日分别标为上旬、中旬、下旬(bins=[0, 10, 20, 31],右端闭合,恰好覆盖大月的31日)。绘图上,sns.lineplotx 由“购药星期”换成新的“旬”列,estimator='sum' 不变。

    # 变式代码:星期维度换成“上旬/中旬/下旬”(特征构造新增两行,绘图改一处)
    df['购药日期'] = df['购药时间'].str.split(pat=' ', expand=True)[0]  # 与平台任务一致:先按空格切出日期段
    df['购药日'] = df['购药日期'].str.split(pat='-', expand=True)[2].astype(int)  # ← 新增行:再按“-”取出日并转整数
    df['旬'] = pd.cut(df['购药日'], bins=[0, 10, 20, 31], labels=['上旬', '中旬', '下旬'])  # ← 新增行:1—10上旬、11—20中旬、21—31下旬
    plt.figure(figsize=(15, 10))  # 与平台任务一致
    plt.grid()  # 与平台任务一致
    sns.lineplot(x='旬', y='实收金额', data=df, estimator='sum')  # ← 改动点:x轴由“购药星期”换成“旬”
    plt.savefig('2_tenth.png')  # 建议另存新文件名,避免覆盖平台任务的2.png
    plt.close()  # 与平台任务一致

    结构性判读:图形由7个星期取值的折线变为仅3个取值(上旬、中旬、下旬)的折线,纵轴仍是各段实收金额总和。判读要点:三段高度的相对关系——月内前中后的销售节奏是否均衡,中旬偏高可能与复诊开药周期有关;由于只有3个点,结论应与月度折线(2.png)互证,而不是单独下判断。

    本地演练版(模拟数据):在与正文本地演练同结构的12行合成数据上实跑:

    # 本地演练版:构造“旬”特征并按旬汇总实收金额
    import pandas as pd  # 导入Pandas数据分析库
    df_mini = pd.DataFrame({  # 12行迷你表,购药时间为“日期 空格 星期”格式,同平台数据
        '商品名称': ['开博通', '氨苄西林胶囊', 'TG厄贝沙坦片', '开博通', '氨苄西林胶囊', 'VC银翘片',
                    '开博通', 'TG厄贝沙坦片', '氨苄西林胶囊', 'VC银翘片', '开博通', 'TG厄贝沙坦片'],
        '实收金额': [281.0, 66.96, 19.8, 189.6, 133.92, 45.6, 379.2, 19.8, 100.44, 45.6, 252.8, 19.8],  # 实收金额
        '购药时间': ['2018-01-05 星期五', '2018-01-28 星期日', '2018-02-02 星期五', '2018-02-14 星期三', '2018-02-25 星期日', '2018-03-02 星期五',
                    '2018-03-11 星期日', '2018-03-22 星期四', '2018-03-25 星期日', '2018-04-03 星期二', '2018-04-18 星期三', '2018-04-29 星期日']  # 日期+星期
    })
    df_mini['购药日期'] = df_mini['购药时间'].str.split(pat=' ', expand=True)[0]  # 与平台任务一致:先切出日期段
    df_mini['购药日'] = df_mini['购药日期'].str.split(pat='-', expand=True)[2].astype(int)  # 变式新增:取出“日”并转整数
    df_mini['旬'] = pd.cut(df_mini['购药日'], bins=[0, 10, 20, 31], labels=['上旬', '中旬', '下旬'])  # 变式新增:划分上中下旬
    tenth_table = df_mini.groupby('旬', observed=True)['实收金额'].sum()  # 变式新增:按旬汇总实收金额
    print('上旬/中旬/下旬的实收金额汇总:')
    print(tenth_table)  # 输出汇总表
    print('各旬订单数:')
    print(df_mini['旬'].value_counts())  # 各旬订单笔数

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

    上旬/中旬/下旬的实收金额汇总:
    旬
    上旬    392.00
    中旬    821.60
    下旬    340.92
    Name: 实收金额, dtype: float64
    各旬订单数:
    旬
    下旬    5
    上旬    4
    中旬    3
    Name: count, dtype: int64

    演练判读:迷你表中旬金额最高而下旬订单数最多——金额口径与订单数口径的“最高段”可以不同,读图时先分清纵轴是哪一个。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 24.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第1条(字符串时间列的逐步拆解:空格切日期、短横线取月份,本变式再多取一位“日”)。

  5. 商业判断:若平台结果显示某种非医保药品连续数月进入销售TOP3(具体名次以平台运行结果为准),医院在采购与处方管理上应如何决策?请兼顾医保占比、患者自费负担与药事管理要求。

    参考答案(点开前请先独立完成)

    参考作答框架:本题不设唯一结论,优秀作答应覆盖以下维度。

    • 口径先行:TOP3按什么口径排名(平台判定按实收金额总和,见@lst-ch24-platform-task,题面提示曾写数量口径);“医保占比”按订单数还是金额算;“非医保”是完全自费还是部分自费——三种口径不清,后续判断都站不住。
    • 分析依据:连续数月进入TOP3说明存在持续的真实需求或处方习惯,应先区分是临床刚需、可替代药,还是医生处方偏好;任务4的差额口径(应收减实收)可帮助核实该药确实几乎不走社保减免。
    • 风险考量:采购上,单品种连续高位意味着供应集中风险,备货需与临床需求联动;处方管理上,非医保药高位销售可能加重患者自费负担、影响依从性,也可能触发药事管理与医保监管的合规关注(如辅助用药、抗菌药管理要求)。
    • 模型边界:月度TOP3是描述性排名,不等于因果结论;连续数月也可能由季节性疾病、短期短缺替代等一次性因素驱动,需要更长窗口确认持续性。
    • 还需补充的数据:该药的适应症与诊断分布、同类医保可替代药品的价格与销量、患者自费放弃率与复购率、医保目录调整计划、供应商供货周期。
    • 常见错误作答形态:直接建议“扩大采购”而不做临床与合规审查;或仅因“非医保”就建议限制使用而不顾临床需求;用一个月的排名当作长期趋势。

    回扣本章:对应“本章新增的技能要点”第4条(口径先于代码,排名结论必须连同口径一起报告)。